#include "openglwidget.h"
#include <QDebug>
#include <QtMath>

/// @brief 顶点着色器
static const char *vertexShaderSource =
    "attribute highp vec3 posAttr;\n"
    "attribute lowp vec4 colAttr;\n"
    "varying lowp vec4 col;\n"
    "uniform highp mat4 matrix;\n"
    "void main() {\n"
    "  col=colAttr;\n"
    "  gl_Position=matrix * vec4(posAttr,1.0f);\n"
    "}\n";

/// @brief 片段着色器
static const char *fragmentShaderSource =
    "varying lowp vec4 col;\n"
    "void main() {\n"
    "   gl_FragColor = col;\n"
    "}\n";

OpenglWidget::OpenglWidget(QWidget *parent)
    : QOpenGLWidget(parent), m_Program(nullptr),
      m_posAttr(0), m_colAttr(0), m_norAttr(0), m_matrixUniform(0), m_VBO(0), m_VAO(nullptr), m_scale(1.0f), m_bShowAxis(false)
{
    m_Timer = new QTimer;
    m_Program.reset(new QOpenGLShaderProgram(this));
    m_VAO.reset(new QOpenGLVertexArrayObject());

    m_PointsVertex = QVector<VertexInfo>();
    ResetView();
    for (GLuint i = 0; i < 6; ++i)
    {
        m_Axisindices[i] = i;
    }
}

OpenglWidget::~OpenglWidget()
{
}

void OpenglWidget::initializeGL()
{
    makeCurrent();
    bool binit = true;
    binit &= InitShader();
    if (!binit)
    {
        return;
    }

    initializeOpenGLFunctions();
    glEnable(GL_DEPTH_TEST);

    m_VAO->create();
    glGenBuffers(1, &m_VBO);
    glGenBuffers(1, &m_EBO);

    initCloud();
    changePointCloud();

    QObject::connect(m_Timer, SIGNAL(timeout()), this, SLOT(onTimerOut()));
    m_Timer->start(30);
}

void OpenglWidget::showPointCloud(const std::vector<QVector3D> &cloud)
{
    initPointCloud(cloud);
    changePointCloud();
    ResetView();
    repaint();
}

void OpenglWidget::initPointCloud(const std::vector<QVector3D> &cloud)
{
    m_PointsVertex.clear();
    m_PointsVertex.resize((int)cloud.size() + 6);
    m_box.calculateMinBoundingBox(cloud);

    addAxisData();

    for (int i = 0; i < cloud.size(); i++)
    {
        m_PointsVertex[i + 6].pos[0] = (cloud[i].x() - m_box.getCenterPoint().x());
        m_PointsVertex[i + 6].pos[1] = (cloud[i].y() - m_box.getCenterPoint().y());
        m_PointsVertex[i + 6].pos[2] = (cloud[i].z() - m_box.getCenterPoint().z());
        gray2Pseudocolor(cloud[i], m_PointsVertex[i + 6].color);
        m_PointsVertex[i + 6].normal[0] = 0.0f;
        m_PointsVertex[i + 6].normal[1] = 1.0f;
        m_PointsVertex[i + 6].normal[2] = 0.0f;
    }
}

void OpenglWidget::addAxisData()
{
    float axisLength = std::min(std::min(m_box.width(), m_box.height()), m_box.depth());
    if (m_PointsVertex.size() > 6)
    {
        for (int i = 0; i < 3; i++)
        {
            m_PointsVertex[2 * i] = VertexInfo();
            float color[4] = {0.0, 0.0, 0.0, 1.0};
            color[i] = 1.0;
            for (int j = 0; j < 4; j++)
            {
                m_PointsVertex[2 * i].color[j] = color[j];
                m_PointsVertex[2 * i + 1].color[j] = color[j];
            }
            float pos[3] = {0.0, 0.0, 0.0};
            pos[i] = axisLength;
            for (int j = 0; j < 3; j++)
            {
                m_PointsVertex[2 * i + 1].pos[j] = pos[j];
            }
        }
    }
}

void OpenglWidget::gray2Pseudocolor(const QVector3D pos, float color[4])
{
    float fmin = m_box.getMinPoint().z();
    float fmax = m_box.getMaxPoint().z();
    int colortemp = (int)(((fmax - pos.z()) / (fmax - fmin)) * 255);
    int r, g, b;
    if (colortemp >= 0 && colortemp < 64)
    {
        r = 0;
        g = 254 - 4 * colortemp;
        b = 255;
    }
    else if (colortemp >= 64 && colortemp < 128)
    {
        r = 0;
        g = 4 * colortemp - 254;
        b = 510 - 4 * colortemp;
    }
    else if (colortemp >= 128 && colortemp < 192)
    {
        r = 4 * colortemp - 510;
        g = 255;
        b = 0;
    }
    else if (colortemp >= 192 && colortemp <= 255)
    {
        r = 255;
        g = 1022 - 4 * colortemp;
        b = 0;
    }
    else
    {
        r = 255;
        g = 255;
        b = 255;
    }
    color[0] = r * 1.0f / 255;
    color[1] = g * 1.0f / 255;
    color[2] = b * 1.0f / 255;
    color[3] = 1.0f;
}

void OpenglWidget::changePointCloud()
{
    if (m_PointsVertex.size() <= 0)
    {
        return;
    }
    m_VAO->bind();
    glBindBuffer(GL_ARRAY_BUFFER, m_VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(VertexInfo) * (int)m_PointsVertex.size(), m_PointsVertex.data(), GL_DYNAMIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(m_Axisindices), m_Axisindices, GL_DYNAMIC_DRAW);

    glEnableVertexAttribArray(m_posAttr);
    glVertexAttribPointer(m_posAttr, 3, GL_FLOAT, GL_FALSE, sizeof(VertexInfo), (void *)(offsetof(VertexInfo, pos)));
    glEnableVertexAttribArray(m_colAttr);
    glVertexAttribPointer(m_colAttr, 4, GL_FLOAT, GL_FALSE, sizeof(VertexInfo), (void *)(offsetof(VertexInfo, color)));

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    m_VAO->release();
}

void OpenglWidget::ResetView()
{
    m_lineMove = QVector3D();
    m_rotate = QQuaternion();
    m_rotate *= QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), 180);
    m_scale = 1.0f;
    setBackgroundColor(QVector3D(5.0f / 255.0f, 54.0f / 255.0f, 80.0f / 255.0f));
}

void OpenglWidget::setBackgroundColor(QVector3D color)
{
    m_backgroundColor = QVector4D(color, 1.0f);
}

void OpenglWidget::resizeGL(int w, int h)
{
    const qreal retinaScale = devicePixelRatio();
    glViewport(0, 0, w * retinaScale, h * retinaScale);
    repaint();
}

bool OpenglWidget::InitShader()
{
    bool success = true;
    success &= m_Program->addShaderFromSourceCode(QOpenGLShader::Vertex, vertexShaderSource);
    success &= m_Program->addShaderFromSourceCode(QOpenGLShader::Fragment, fragmentShaderSource);
    success &= m_Program->link();
    GetShaderUniformPara();
    return success;
}

void OpenglWidget::GetShaderUniformPara()
{
    m_posAttr = m_Program->attributeLocation("posAttr");
    m_colAttr = m_Program->attributeLocation("colAttr");
    m_matrixUniform = m_Program->uniformLocation("matrix");
}

void OpenglWidget::paintGL()
{
    makeCurrent();
    m_Program->bind();
    glClearColor(m_backgroundColor.x(), m_backgroundColor.y(), m_backgroundColor.z(), m_backgroundColor.w());
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BITS);

    m_VAO->bind();
    setMatrixUniform();
    glDrawArrays(GL_POINTS, 6, (GLsizei)m_PointsVertex.size() - 6);
    if (m_bShowAxis)
    {
        glEnable(GL_LINE_SMOOTH);
        glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glLineWidth(3.0f);
        glDisable(GL_DEPTH_TEST); // 确保坐标轴在最前

        glDrawElements(GL_LINES, 6, GL_UNSIGNED_INT, 0);

        // glEnable(GL_DEPTH_TEST);
        // glDisable(GL_BLEND);
        // glDisable(GL_LINE_SMOOTH);
        // glLineWidth(1.0f);
    }
    m_VAO->release();
    m_Program->release();
}

void OpenglWidget::setMatrixUniform()
{
    QMatrix4x4 matrix = QMatrix4x4();
    QMatrix4x4 matrixPerspect = QMatrix4x4();
    QMatrix4x4 matrixView = QMatrix4x4();
    QMatrix4x4 matrixModel = QMatrix4x4();

    QVector3D minPos = (m_box.getMinPoint() - m_box.getCenterPoint());
    QVector3D maxPos = (m_box.getMaxPoint() - m_box.getCenterPoint());
    float maxAxis;
    maxAxis = qAbs(qMax(qMax(m_box.depth(), m_box.width()), m_box.height()));

    float aspectRatio = width() * 1.0f / height();
    // TODO 计算正交投影矩阵时考虑当前窗口的宽高比
    matrixPerspect.ortho(-aspectRatio * maxAxis, aspectRatio * maxAxis, -maxAxis, maxAxis, -2 * maxAxis, 2 * maxAxis);

    matrixView.lookAt(QVector3D(0, 0, maxAxis), QVector3D(0.0, 0.0, -1), QVector3D(0.0, 1.0, 0.0));
    matrixView.translate(m_lineMove.x(), m_lineMove.y(), m_lineMove.z());

    matrixModel.rotate(m_rotate);
    matrixModel.scale(m_scale);

    matrix = matrixPerspect * matrixView * matrixModel;
    m_Program->setUniformValue(m_matrixUniform, matrix);
}

void OpenglWidget::initCloud()
{
    m_PointsVertex.clear();
    VertexInfo point;
    point.pos[0] = 0.0f;
    point.pos[1] = 0.0f;
    point.pos[2] = 0.0f;
    point.color[0] = m_backgroundColor.x();
    point.color[1] = m_backgroundColor.y();
    point.color[2] = m_backgroundColor.z();
    point.color[3] = m_backgroundColor.w();
    point.normal[0] = 0.0f;
    point.normal[1] = 1.0f;
    point.normal[2] = 0.0f;
    m_PointsVertex.push_back(point);
}

void OpenglWidget::onTimerOut()
{
    if (this->isVisible())
    {
        repaint();
    }
}

void OpenglWidget::mousePressEvent(QMouseEvent *e)
{
    if (e->buttons() & Qt::LeftButton || e->buttons() & Qt::MidButton)
    {
        setMouseTracking(true);
        m_lastPoint = QVector2D(e->localPos());
        // 开始拖拽时显示坐标系
        m_bShowAxis = true;
        repaint();
    }
    if (e->button() == Qt::RightButton)
    {
        // 记录右键按下的初始位置
        m_rightButtonInitialPos = QVector2D(e->localPos());
    }
}

void OpenglWidget::mouseMoveEvent(QMouseEvent *e)
{
    if (e->buttons() & Qt::LeftButton)
    {
        Rotate(QVector2D(m_lastPoint), QVector2D(e->localPos()));
    }
    if (e->buttons() & Qt::RightButton)
    {
        // 使用右键按下的初始位置和当前位置来计算移动
        LineMove(m_rightButtonInitialPos, QVector2D(e->localPos()));
        // 更新初始位置为当前位置，以便下一次移动计算
        m_rightButtonInitialPos = QVector2D(e->localPos());
    }
    m_lastPoint = QVector2D(e->localPos());
    repaint();
}

void OpenglWidget::mouseReleaseEvent(QMouseEvent *e)
{
    setMouseTracking(false);
    // 松开鼠标时隐藏坐标系
    m_bShowAxis = false;
    repaint();
}

void OpenglWidget::wheelEvent(QWheelEvent *e)
{
    if (e->delta() > 0)
    {
        modelZoomInOrOut(true);
    }
    else
    {
        modelZoomInOrOut(false);
    }
}

void OpenglWidget::keyPressEvent(QKeyEvent *e)
{
    if (e->key() == Qt::Key_Space)
    {
        ResetView();
        // return;
    }
    // else if ((e->key() == Qt::Key_C) && (e->modifiers() == Qt::ControlModifier))
    // {
    //     m_bShowAxis = !m_bShowAxis;
    // }
    QWidget::keyPressEvent(e);
}

void OpenglWidget::leaveEvent(QEvent *)
{
    // 不释放键盘事件抓取，会让编辑框无法输入
    releaseKeyboard();
}

void OpenglWidget::enterEvent(QEvent *)
{
    grabKeyboard();
}

void OpenglWidget::LineMove(QVector2D posOrgin, QVector2D posEnd)
{
    float ratio = 0.003f * sqrt(m_box.width() * m_box.height());
    float xoffset = posEnd.x() - posOrgin.x();
    float yoffset = posEnd.y() - posOrgin.y();

    // 仅基于鼠标的相对移动来更新 m_lineMove
    m_lineMove += QVector3D(xoffset * ratio, -yoffset * ratio, 0);
}

void OpenglWidget::Rotate(QVector2D posOrgin, QVector2D posEnd)
{
    QVector2D diff = posEnd - posOrgin;
    qreal acc = diff.length() / 100.0;
    if (acc < 0.01f)
    {
        return;
    }
    calRotation(posOrgin, posEnd);
}

void OpenglWidget::modelZoomInOrOut(bool ZoomInOrOut)
{
    if (ZoomInOrOut) // zoom in
    {
        m_scale *= 1.1f;
        //        if(m_scale>2.0f)
        //            m_scale = 2.0f;
    }
    else
    { // zoom out
        m_scale *= 0.9f;
        if (m_scale < 0.5f)
            m_scale = 0.5f;
    }
}

void OpenglWidget::calRotation(QVector2D posOrgin, QVector2D posEnd)
{
    QVector3D orginViewPos = pixelPosToViewPos(posOrgin);
    QVector3D endViewPos = pixelPosToViewPos(posEnd);
    float RotateAngle;
    RotateAngle = qRadiansToDegrees(std::acos(QVector3D::dotProduct(orginViewPos, endViewPos)));
    QVector3D axis;
    axis = QVector3D::crossProduct(orginViewPos, endViewPos);
    axis.normalize();
    m_rotate = QQuaternion::fromAxisAndAngle(axis, RotateAngle) * m_rotate;
}

QVector3D OpenglWidget::pixelPosToViewPos(const QVector2D &p)
{
    QVector3D viewPos(2.0 * float(p.x()) / width() - 1.0,
                      1.0 - 2.0 * float(p.y()) / height(),
                      0);
    float sqrZ = 1 - QVector3D::dotProduct(viewPos, viewPos);
    if (sqrZ > 0)
    {
        viewPos.setZ(std::sqrt(sqrZ));
    }
    else
    {
        viewPos.normalize();
    }
    return viewPos;
}
